<template>
  <div class="container" :style="containerStyle" ref="containerRef">
    <div class="item" :style="itemStyle" :index="idx" v-for="i, idx in data">
      <slot name="item" :itemData="i"></slot>
    </div>
    <div v-show="rectShow" class="rectArea" ref="rectAreaRef"
      :style="{ border: rectBorderStyle, background: rectBackgroundStyle }"></div>
  </div>
</template>

<script>
export default {
  name: 'AreaSelector',
  props: {
    // 选框边框样式
    rectBorderStyle: {
      type: String,
      default: () => '1px solid rgb(0,119,255)'
    },
    // 选框背景色
    rectBackgroundStyle: {
      type: String,
      default: () => 'rgba(0,119,255,0.2)'
    },
    // 框选区域样式
    containerStyle: {
      type: Object,
      default: () => {}
    },
    // 框选元素区域样式
    itemStyle: {
      type: Object,
      default: () => {}
    },
    // 回传的data属性键
    dataKey: {
      type: String,
      default: () => 'id'
    },
    // 传入的数据
    data: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      rectShow: false,
      ctrlKey: false,
      tempSelectedIds: [],
      unselectedIds: [],
      selectedIds: [],
      startPoint: null,
      endPoint: null,
      containerWidth: 0,
      containerHeight: 0
    }
  },
  mounted() {
    this.handleMouseDown()
    this.handleMouseUp()
  },
  methods: {
    handleMouseDown() {
      this.$refs.containerRef.addEventListener('mousedown', e => {
        const { clientX, clientY, ctrlKey } = e;
        this.ctrlKey = ctrlKey;
        this.tempSelectedIds = [];
        this.unselectedIds = [];
        this.startPoint = this.getRelativePositionInElement(clientX, clientY);
        this.endPoint = this.startPoint;
        this.getContainerRect()
        this.updateArea();
        this.rectShow = true
        this.handleMouseMove();
      })
    },
    getRelativePositionInElement(clientX, clientY) {
      const rect = this.$refs.containerRef.getBoundingClientRect();
      const { left, top } = rect;
      const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = this.$refs.containerRef;
      let x = clientX - left + scrollLeft;
      let y = clientY - top + scrollTop;
      if (x < 0) {
        x = 0;
      } else if (x > scrollWidth) {
        x = scrollWidth;
      }

      if (y < 0) {
        y = 0;
      } else if (y > scrollHeight) {
        y = scrollHeight;
      }

      return { x: Math.round(x), y: Math.round(y) };
    },
    getContainerRect() {
      this.containerWidth = this.$refs.containerRef.scrollWidth
      this.containerHeight = this.$refs.containerRef.scrollHeight
    },
    updateArea() {
      const top = Math.min(this.startPoint.y, this.endPoint.y);
      const left = Math.min(this.startPoint.x, this.endPoint.x);
      const width = Math.abs(this.startPoint.x - this.endPoint.x);
      const height = Math.abs(this.startPoint.y - this.endPoint.y);

      // // 防止矩形超出容器右边和下边
      const finalWidth = Math.min(width, this.containerWidth - left) - 2;
      const finalHeight = Math.min(height, this.containerHeight - top) - 2;

      // 更新矩形的位置和大小
      this.setRectArea(top, left, finalWidth, finalHeight);
      this.selectItems();
    },
    setRectArea(top, left, width, height) {
      this.$refs.rectAreaRef.style.top = top + 'px';
      this.$refs.rectAreaRef.style.left = left + 'px';
      this.$refs.rectAreaRef.style.width = width + 'px';
      this.$refs.rectAreaRef.style.height = height + 'px';
    },
    selectItems() {
      const areaRect = this.$refs.rectAreaRef.getBoundingClientRect();
      const items = this.$refs.containerRef.querySelectorAll(".item")
      for (const item of items) {
        const itemRect = item.getBoundingClientRect();
        const hasIntersection = this.twoRectsHaveIntersection(areaRect, itemRect);
        const itemId = this.data[item.getAttribute('index')][this.dataKey];
        const idx = this.selectedIds.indexOf(itemId);
        const tempIdx = this.tempSelectedIds.indexOf(itemId);
        let selected;

        if (this.unselectedIds.includes(itemId)) {
          selected = false;
        } else {
          if (this.ctrlKey) {
            if (idx >= 0) {
              if (hasIntersection) {
                // 已经选中的元素如果再被框住则反选
                selected = false;
                this.unselectedIds.push(itemId);
              } else {
                // 已选中的元素保持选中状态
                selected = true;
              }
            } else {
              // 本次是否选中通过是否和 areaRect 有重叠判断
              selected = hasIntersection;
            }
          } else {
            selected = hasIntersection;
          }
        }

        if (selected) {
          if (tempIdx === -1) {
            this.tempSelectedIds.push(itemId);
          }
        } else {
          if (tempIdx >= 0) {
            this.tempSelectedIds.splice(tempIdx, 1);
          }
          if (idx >= 0) {
            this.selectedIds.splice(idx, 1);
          }
        }
      }
    },
    twoRectsHaveIntersection(rect1, rect2) {
      const left1 = rect1.left;
      const left2 = rect2.left;
      const right1 = rect1.left + rect1.width;
      const right2 = rect2.left + rect2.width;

      const top1 = rect1.top;
      const top2 = rect2.top;
      const bottom1 = rect1.top + rect1.height;
      const bottom2 = rect2.top + rect2.height;

      const width1 = rect1.width;
      const width2 = rect2.width;
      const height1 = rect1.height;
      const height2 = rect2.height;

      const noIntersection = left2 > right1 || left1 > right2 || bottom1 < top2 || bottom2 < top1 || width1 <= 0 || width2 <= 0 || height1 <= 0 || height2 <= 0;

      return !noIntersection;
    },
    handleMouseMove() {
      window.addEventListener('mousemove', this.mouseMoveHandler);
    },
    mouseMoveHandler(e) {
      const { clientX, clientY } = e;
      this.endPoint = this.getRelativePositionInElement(clientX, clientY);
      this.updateArea();
      this.scrollOnDrag(clientX, clientY)
    },
    handleMouseUp() {
      window.addEventListener('mouseup', e => {
        window.removeEventListener('mousemove', this.mouseMoveHandler);
        this.rectShow = false
        this.setRectArea(0, 0, 0, 0);
        const updated = Array.from(new Set([...this.selectedIds, ...this.tempSelectedIds]));
        this.selectedIds = updated;
        this.$emit('change', updated);
      });
    },
    scrollOnDrag(mouseX, mouseY) {
      const { x, y, width, height } = this.$refs.containerRef.getBoundingClientRect();

      let scrollX, scrollY;

      if (mouseX < x) {
        scrollX = mouseX - x;
      } else if (mouseX > (x + width)) {
        scrollX = mouseX - (x + width);
      }

      if (mouseY < y) {
        scrollY = mouseY - y;
      } else if (mouseY > (y + height)) {
        scrollY = mouseY - (y + height);
      }
      if (scrollX || scrollY) {
        this.$refs.containerRef.scrollBy({
          left: scrollX,
          top: scrollY,
          behavior: 'auto'
        });
      }
    }
  }
}
</script>

<style scoped>
.container {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  height: 100%;
  overflow: auto;
  .item {
    width: 100px;
    height: 100px;
    margin: 3px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 5px;
    position: relative;
    border: #dcdfe6 solid 1px;
    flex-direction: column;
    user-select: none;
  }

  .rectArea {
    position: absolute;
  }
}
</style>